﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using chromium.HTTP.Parser;
using chromium.HTTP.Runtime;
using chromium.Log;
using chromium.Utils;

namespace chromium.GAE.Simple2Async
{
    public class SimpleAsyncHandler1 : IAsyncHttpRequestHandler
    {
        private static readonly ILog logger = LogManager.GetLogger(typeof(SimpleAsyncHandler2));
        private AsyncCallback endProcessResponseCallBack;
        private AsyncCallback endReadHeaderCallBack;

        public SimpleAsyncHandler1()
        {
            endProcessResponseCallBack = new AsyncCallback(EndProcessResponse);
            endReadHeaderCallBack = new AsyncCallback(EndReadHeader);
        }

        public virtual void BeginExecute(HandlerContext context)
        {
            BeginGetRequestToProxy(context);
        }

        protected virtual int GetHeaderSize(HttpWebResponse httpWebResponse)
        {
            string[] len = httpWebResponse.GetResponseHeader("X-Length").Split(',');
            return int.Parse(len[0]);
        }

        protected virtual int GetContentSize(HttpWebResponse httpWebResponse)
        {
            string[] len = httpWebResponse.GetResponseHeader("X-Length").Split(',');
            return int.Parse(len[1]);
        }

        protected virtual byte GetKey(HttpWebResponse httpWebResponse)
        {
            int key = int.Parse(httpWebResponse.GetResponseHeader("X-Key"));
            return (byte)(key < 0 ? key + 256 : key);
        }

        private void EndReadHeader(IAsyncResult ar)
        {
            AsyncRequestContext requestContext = (AsyncRequestContext)ar.AsyncState;
            int count;
            try
            {
                count = requestContext.EndRead(ar);
                if (count <= 0)
                {
                    requestContext.Cleanup();
                    return;
                }
            }
            catch (Exception e)
            {
                requestContext.AddException(e);
                requestContext.Cleanup();
                return;
            }

            requestContext.HeaderReaded += count;

            if (requestContext.HeaderReaded < requestContext.HeaderLength)
            {
                requestContext.BeginRead(requestContext.Buffer, requestContext.HeaderReaded, requestContext.HeaderLength - requestContext.HeaderReaded, endReadHeaderCallBack, requestContext);
            }
            else
            {
                ProcessHeader(requestContext);

                requestContext.BeginRead(requestContext.Buffer, 0, Math.Min(requestContext.Buffer.Length, requestContext.ContentLength), endProcessResponseCallBack, requestContext);
            }
        }

        private void ProcessHeader(AsyncRequestContext requestContext)
        {
            var ep = new XorEncryptor { Key = GetKey(requestContext.ResponseFromPoxy) };

            ep.Encrypt(requestContext.Buffer, 0, requestContext.HeaderReaded);

            string headerText = Encoding.UTF8.GetString(requestContext.Buffer, 0, requestContext.HeaderReaded);

            string[] headers = headerText.Split(new[] { "\n" }, StringSplitOptions.RemoveEmptyEntries);

            requestContext.Response.Status = int.Parse(headers[0]);

            requestContext.Response.ContentLength = requestContext.ContentLength;

            for (int i = 1; i < headers.Length; i++)
            {
                int pos = headers[i].IndexOf(':');
                if (pos < 0) continue;

                string header = headers[i].Substring(0, pos).Trim();
                string value = headers[i].Substring(pos + 1).Trim();

                if (header.Equals("set-cookie", StringComparison.OrdinalIgnoreCase))
                {
                    ProcessCooike(requestContext.Response, value);
                }
                else requestContext.Response.AddHeader(header, value);
            }
        }

        private void EndProcessResponse(IAsyncResult ar)
        {
            AsyncRequestContext requestContext = (AsyncRequestContext)ar.AsyncState;

            int readed;
            try
            {
                readed = requestContext.EndRead(ar);
                if (readed <= 0)
                {
                    requestContext.Cleanup();
                    return;
                }
            }
            catch (Exception e)
            {
                requestContext.AddException(e);
                requestContext.Cleanup();
                return;
            }

            requestContext.ContentSizeReaded += readed;

            XorEncryptor ep = new XorEncryptor { Key = GetKey(requestContext.ResponseFromPoxy) };
            ep.Encrypt(requestContext.Buffer, 0, readed);

            try
            {
                requestContext.Response.Write(requestContext.Buffer, 0, readed);
            }
            catch (Exception e)
            {
                requestContext.AddException(e);
                requestContext.Cleanup();
                return;
            }

            if (requestContext.ContentSizeReaded < requestContext.ContentLength)
            {
                requestContext.BeginRead(requestContext.Buffer, 0, Math.Min(requestContext.Buffer.Length, requestContext.ContentLength - requestContext.ContentSizeReaded), endProcessResponseCallBack, requestContext);
            }
            else
            {
                requestContext.Cleanup();
                logger.DebugFormat("{0}-{1} {2} {3}ms", requestContext.RequestToProxy.Host, requestContext.Request.RawUrl, requestContext.Response.Status, requestContext.Request.ElapsedMs);
            }
        }

        protected virtual void ProcessCooike(HttpResponse response, string setCookies)
        {
            if (string.IsNullOrEmpty(setCookies)) return;

            string[] cookies = setCookies.Split(new string[] { ", " }, StringSplitOptions.RemoveEmptyEntries);

            List<string> cookieHeader = new List<string>();

            foreach (string cookie in cookies)
            {
                int x = cookie.IndexOf("=", StringComparison.Ordinal);
                int y = cookie.IndexOf(";", StringComparison.Ordinal);
                if (x < 0 || (y > -1 && x > y))
                {
                    cookieHeader[cookieHeader.Count - 1] = string.Format("{0}, {1}", cookieHeader[cookieHeader.Count - 1], cookie);
                }
                else
                {
                    cookieHeader.Add(cookie);
                }
            }

            foreach (string match in cookieHeader)
            {
                response.AddCookie(match);
            }
        }

        protected virtual void BeginGetRequestToProxy(HandlerContext context)
        {
            string url = string.Format("{0}://{1}:{2}/{3}", Settings.Https ? "https" : "http", DnsHelper.GetHost(context.Request.Host), Settings.SpdyPort, Settings.Mode);
            HttpWebRequest requestToProxy = (HttpWebRequest)WebRequest.Create(url);
            requestToProxy.Host = DnsHelper.GetGaeHost(context.Request.Host);
            requestToProxy.Method = "POST";
            requestToProxy.AllowAutoRedirect = false;
            requestToProxy.Proxy = EmptyProxy.Instance;

            AsyncRequestContext requestContext = new AsyncRequestContext(context)
                                                     {
                                                         RequestToProxy = requestToProxy
                                                     };
            requestContext.BeginGetRequestStream(EndGetRequestStream, requestContext);
        }

        private void EndGetRequestStream(IAsyncResult ar)
        {
            AsyncRequestContext requestContext = (AsyncRequestContext)ar.AsyncState;
            try
            {
                requestContext.RequestStream = requestContext.EndGetRequestStream(ar);
            }
            catch (Exception e)
            {
                requestContext.AddException(e);
                requestContext.Cleanup();
                return;
            }

            using (BlockMemoryStream requestStream = new BlockMemoryStream(2000))
            {
                StringBuilder sb = new StringBuilder();
                foreach (KeyValuePair<string, string> keyValuePair in requestContext.Request.Headers.Where(keyValuePair => Guard.IsSafeHeader(keyValuePair.Key)))
                {
                    sb.AppendFormat("{0}:{1}\n", keyValuePair.Key, keyValuePair.Value);
                }
                requestStream.WriteBlock(sb.ToString());
                requestStream.WriteBlock(requestContext.Request.Method);
                requestStream.WriteBlock(requestContext.Request.RawUrl);
                requestStream.WriteBlock(requestContext.Request.RequestStream.ToArray());
                requestStream.Flush();

                requestContext.BeginWrite(requestStream.ToArray(), EndWriteRequest, requestContext);
            }
        }

        private void EndWriteRequest(IAsyncResult ar)
        {
            AsyncRequestContext requestContext = (AsyncRequestContext)ar.AsyncState;
            try
            {
                requestContext.EndWrite(ar);
            }
            catch (Exception e)
            {
                requestContext.AddException(e);
                requestContext.Cleanup();
                return;
            }
            requestContext.BeginGetResponse(EndGetResponse, requestContext);
        }

        private void EndGetResponse(IAsyncResult ar)
        {
            AsyncRequestContext requestContext = (AsyncRequestContext)ar.AsyncState;
            HttpWebResponse response;
            try
            {
                response = requestContext.EndGetResponse(ar);
            }
            catch (Exception e)
            {
                requestContext.AddException(e);
                requestContext.Cleanup();
                return;
            }
            requestContext.ResponseFromPoxy = response;

            requestContext.HeaderLength = GetHeaderSize(response);

            requestContext.ContentLength = GetContentSize(response);

            requestContext.ResponseStreamFromPoxy = response.GetResponseStream();

            requestContext.EnsureBuffer();

            requestContext.BeginRead(requestContext.Buffer, 0, requestContext.HeaderLength, endReadHeaderCallBack, requestContext);
        }

        public class AsyncRequestContext
        {
            private readonly HandlerContext context;
            private readonly List<Exception> exceptions = new List<Exception>();

            public AsyncRequestContext(HandlerContext context)
            {
                this.context = context;
            }

            public HttpRequest Request
            {
                get { return context.Request; }
            }

            public HttpResponse Response
            {
                get { return context.Response; }
            }

            public HttpWebRequest RequestToProxy { get; set; }

            public Stream RequestStream { get; set; }

            public HttpWebResponse ResponseFromPoxy { get; set; }

            public Stream ResponseStreamFromPoxy { get; set; }

            public int HeaderReaded { get; set; }

            public int HeaderLength { get; set; }

            public int ContentLength { get; set; }

            public byte[] Buffer { get; set; }

            public int ContentSizeReaded { get; set; }

            public void BeginGetRequestStream(AsyncCallback callback, AsyncRequestContext requestContext)
            {
                RequestToProxy.BeginGetRequestStream(callback, requestContext);
            }

            public Stream EndGetRequestStream(IAsyncResult ar)
            {
                return RequestToProxy.EndGetRequestStream(ar);
            }

            public void BeginWrite(byte[] toArray, AsyncCallback callback, AsyncRequestContext requestContext)
            {
                RequestStream.BeginWrite(toArray, 0, toArray.Length, callback, requestContext);
            }

            public void EndWrite(IAsyncResult ar)
            {
                RequestStream.EndWrite(ar);
            }

            public void BeginGetResponse(AsyncCallback callback, AsyncRequestContext requestContext)
            {
                RequestToProxy.BeginGetResponse(callback, requestContext);
            }

            public HttpWebResponse EndGetResponse(IAsyncResult ar)
            {
                return (HttpWebResponse)RequestToProxy.EndGetResponse(ar);
            }

            public void BeginRead(byte[] buffer, int start, int length, AsyncCallback callback, AsyncRequestContext context)
            {
                ResponseStreamFromPoxy.BeginRead(buffer, start, length, callback, context);
            }

            public int EndRead(IAsyncResult ar)
            {
                return ResponseStreamFromPoxy.EndRead(ar);
            }

            public void Cleanup()
            {
                try
                {

                    if (ResponseStreamFromPoxy != null)
                        ResponseStreamFromPoxy.Dispose();

                    if (ResponseFromPoxy != null)
                        ResponseFromPoxy.Close();

                    if (RequestStream != null)
                        RequestStream.Close();

                    if (RequestToProxy != null && ResponseFromPoxy == null)
                        RequestToProxy.Abort();

                    if (exceptions.Count == 0)
                    {
                        Response.Close();
                        context.PerformCallback();
                    }
                    else
                        Response.Abort();
                }
                catch (Exception e)
                {
                    logger.Error("Cleanup exception", e);
                }
            }

            public void AddException(Exception exception)
            {
                logger.Error(exception.Message, exception);

                exceptions.Add(exception);
            }

            public void EnsureBuffer()
            {
                int size = Math.Max(2048, HeaderLength);
                Buffer = new byte[size];
            }
        }
    }
}